Skip to content

Fix: prevent unexpected navigation when destroying record from side panel#21391

Open
DeviSriSaiCharan wants to merge 15 commits into
twentyhq:mainfrom
DeviSriSaiCharan:fix/prevent-unexpected-navigation-when-destroying-record-from-side-panel
Open

Fix: prevent unexpected navigation when destroying record from side panel#21391
DeviSriSaiCharan wants to merge 15 commits into
twentyhq:mainfrom
DeviSriSaiCharan:fix/prevent-unexpected-navigation-when-destroying-record-from-side-panel

Conversation

@DeviSriSaiCharan

@DeviSriSaiCharan DeviSriSaiCharan commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Fixes: #21243

Issue

When a user is on a specific record's page (for example, looking at a Person) and opens a related record (like a Note) in the right-side panel, clicking "Permanently Delete" on that Note would abruptly redirect the user to the main "Notes" list. This breaks the user's workflow, as they typically want to remain on the parent Person page after deleting a sub-record.

Root Cause

Inside the DestroyRecordsCommand and DeleteRecordsCommand components, the application was programmed to unconditionally trigger a navigateApp redirect to the deleted object's index page upon successful deletion. The code did not account for whether the deletion was triggered from the main index page or from inside a contextual side panel.

How we fixed it

I introduced a new isInSidePanel flag into the HeadlessCommandContextApi.
The command menu now detects if the delete action originated from inside a side panel and passes this flag down the execution chain. If true, the DestroyRecordsCommand simply closes the panel (closeSidePanelMenu()) and keeps the user exactly where they were.

After that fix, I encountered another issue (UI not updating automatically)

Because the app now correctly kept the user on the Person page, a new bug surfaced: the "Note" chip inside the relation table did not disappear immediately. The user had to manually refresh the page to see the deletion.

This happened because:

  1. The Apollo optimistic cache occasionally failed to trace deeply nested morph-relationships back to the parent.
  2. A standard local React useState fallback was insufficient. The table component aggressively unmounts and remounts relation cells whenever a user hovers over them to display interactive controls, which would wipe the local React state clean and cause the "ghost chip" to reappear.

##How I fixed that issue (and optimized it)
I built an event-driven fallback using global Jotai state to permanently hide the chips:

  1. Precision ID Broadcasting: I updated the useDestroyManyRecords and useIncrementalDestroyManyRecords hooks to extract and broadcast only the confirmed destroyed IDs directly from the Apollo mutation response, preventing false-positive UI removals if the backend performed a partial delete.
  2. Batch Optimizations: During bulk deletions, events are now broadcasted per-batch rather than accumulating thousands of IDs in memory until the end, keeping the UI instantly responsive and memory bounded.
  3. Per-Cell State Scoping (atomFamily): Instead of a leaky global array, we used Jotai's atomFamily to dynamically generate a unique Noticeboard for every specific table cell (${recordId}-${fieldName}). This ensures the hidden-state survives mouse-hover unmounts while remaining cleanly isolated.
  4. Defensive Filtering: The RelationFromManyFieldDisplay component was updated to defensively guard against undefined array entries and strictly match deleted IDs only against valid foreign keys (ending in 'Id'), preventing false-positive removals if a UUID happened to be pasted into a description field.
  5. SSE Resilience: We hardened the Server-Sent Event (SSE) listeners with optional chaining and null-filtering to ensure malformed backend payloads do not crash the real-time event pipeline.

Screen Recording

Screen.Recording.2026-06-10.at.11.04.43.AM.mov

… people records page

- This commit introduces isInSidePanel flag into the HeadlessCommandContextApi, passing it down from the command menu click event. DestroyRecordsCommand now check this flagl if the deletion originates from a side panel, the app simply closes the panel and keeps the user on their current page.
- Updated ObjectRecordOperation to include the specific destroyRecordIds in the browser event payload.
- Introduced a global locallyDeletedRecordIdsAtom so the list of deleted records survives cell unmount/remounts during mouse hovers.
- Updated RelationfromManyFieldDisplay to listen for destruction events, instantly log the destroyed IDs to the global state, and forcefully filter them out of the UI
Copilot AI review requested due to automatic review settings June 10, 2026 06:18
@twenty-ci-bot-public

Copy link
Copy Markdown

👋 Thanks for contributing to Twenty!

Your PR has been set to draft while you work on it. Once you're done, mark it as Ready for review and our automated checks will run.

Looking forward to your contribution!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Propagates destroyed record IDs through ObjectRecordOperation browser events and uses them in relation field displays, plus adds context about whether a command is executed from the side panel to adjust post-destroy UI behavior.

Changes:

  • Extend ObjectRecordOperation to include destroyedRecordId(s) for destroy operations and populate them at dispatch / SSE conversion points.
  • Filter RelationFromManyFieldDisplay results based on locally tracked destroyed IDs from browser events.
  • Add isInSidePanel to the headless command context and close the side panel instead of navigating after single-record destroy.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/twenty-front/src/modules/sse-db-event/utils/turnSseObjectRecordEventToObjectRecordOperationBrowserEvent.ts Adds destroyed record id(s) onto destroy operations derived from SSE events
packages/twenty-front/src/modules/object-record/types/ObjectRecordOperation.ts Refines destroy operation union types to require destroyed id(s)
packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RelationFromManyFieldDisplay.tsx Tracks destroyed ids and filters displayed relation values accordingly
packages/twenty-front/src/modules/object-record/hooks/useIncrementalDestroyManyRecords.ts Collects destroyed ids across batches and dispatches them in a browser event
packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts Dispatches destroyed record id for destroy-one operations
packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts Dispatches destroyed record ids for destroy-many operations
packages/twenty-front/src/modules/command-menu-item/hooks/useCommandMenuItemClick.ts Passes isInSidePanel when mounting commands
packages/twenty-front/src/modules/command-menu-item/engine-command/utils/buildHeadlessCommandContextApi.ts Threads isInSidePanel through the headless command context
packages/twenty-front/src/modules/command-menu-item/engine-command/types/HeadlessCommandContextApi.ts Adds isInSidePanel to the headless context type
packages/twenty-front/src/modules/command-menu-item/engine-command/record/components/DestroyRecordsCommand.tsx Closes side panel after single-record destroy when executed from side panel
packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountCommand.ts Extends mount params to accept isInSidePanel

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 11 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts Outdated
DeviSriSaiCharan and others added 10 commits June 10, 2026 12:05
… using atomFamily

Previously, the `locallyDeletedRecordIdsAtom` (used to instantly hide destroyed chips in `RelationFromManyFieldDisplay`) was defined as a single global array. This raised architectural concerns as it could create a small memory leak over long sessions (an ever-growing array) and caused all table cells across the application to share and iterate over the same list of destroyed IDs.
This commit refactors the state to use Jotai's `atomFamily`, keying the state dynamically by `${recordId}-${fieldName}`. This ensures the fallback state remains cleanly isolated per-cell while still surviving component unmounts/remounts during mouse hovers.
This commit resolves PR feedback regarding the event-driven fallback in RelationFromManyFieldDisplay:
- Removed the empty string fallback (`?? ''`) for `objectMetadataItemId` to safely pass `undefined` and prevent the listener from subscribing to invalid keys.
- Enforced strict equality (`===`) for the locally deleted IDs length check.
- Wrapped state updates in `Array.from(new Set(...))` to guarantee ID uniqueness, ensuring fast lookups and preventing array bloat from duplicate destroy events.
…ve performance

During bulk deletions, `useIncrementalDestroyManyRecords` was accumulating all destroyed IDs in memory and waiting until the entire operation finished before broadcasting the UI event. For massive deletions, this caused unnecessary memory bloat and delayed UI responsiveness.

This commit removes the accumulation array and moves the event dispatch inside the `incrementalFetchAndMutate` loop. Destroy events are now broadcast progressively per-batch, bounding memory usage and ensuring the UI updates incrementally.
@twenty-ci-bot-public

Copy link
Copy Markdown

🔍 Automated Pre-Review

No issues detected - This PR is ready for human review.


🧭 External PR Quality Review

🟠 Needs triage for the following reason(s):

  • Does not follow Twenty technical standards (3 issue(s))

cc @prastoin

Checks

  • CI: pending
  • 🔗 Linked issue: Fixes: #21243
  • 📐 Standards: 3 issue(s)

Detailed findings (duplicate candidates, standards notes, summary) are in the workflow run logs.


View details

Automated pre-review — human approval still required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Permanently destroying a record in the side panel redirects to that object’s list

3 participants